package com.hup.timer.view;

import com.hup.timer.constant.TimerConstant;
import com.hup.timer.model.TimeViewConfig;
import com.hup.timer.model.TimerConfig;
import com.hup.timer.mvp.MainMvp.MainPresenter;
import com.hup.timer.service.gesture.TimerWindowGesture;
import com.hup.timer.service.gesture.TimerWindowGesture.GestureCallback;
import com.hup.timer.view.customView.TimeView;
import com.hup.utils.commons.exception.AppExceptionHandler;
import com.hup.utils.commons.exception.ErrInfo;
import com.hup.utils.commons.hardware.ScreenUtil;
import com.hup.utils.commons.hardware.ScreenUtil.ScreenChangedManager.ScreenListener;
import com.hup.utils.commons.start.AppIcon;
import com.hup.utils.commons.start.DialogHelper;
import com.hup.utils.commons.start.SimpleExitHandler;
import com.hup.utils.commons.start.SpringContext;
import com.hup.utils.commons.thread.ThreadPoolUtil;
import com.hup.utils.commons.thread.ThreadUtil;
import com.hup.utils.swing.FrameUtil;
import com.hup.utils.swing.frame.AutoHideMenu;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;

import javax.swing.JCheckBoxMenuItem;
import javax.swing.JWindow;
import java.awt.Dimension;
import java.awt.DisplayMode;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author hugan
 * @date 2020/1/9
 */
@Log4j2
public class TimerWindow extends JWindow implements ScreenListener {

    private final ThreadPoolExecutor flashExecutor;
    private MainPresenter presenter;
    private TimerConfig config;
    private final Point bootLoc;

    @Getter
    private TimeView timeView;
    private WindowMenu windowMenu;
    /**
     * 由用户或本程序
     */
    private Point lastLocation;

    /**
     * 无鼠标穿透的window属性值
     */
    private Integer windowStyleDefault;

    public TimerWindow() {
        presenter = SpringContext.getBean(MainPresenter.class);
        config = presenter.getConfig();
        bootLoc = config.timeWindowConfig.loc;
        flashExecutor = initFlashExecutor();

        setAlwaysOnTop(true);

        initView();
        initListener();
        applyConfig(config);
        afterLocationChanged();
    }

    private ThreadPoolExecutor initFlashExecutor() {
        //核心线程0,最大线程1,有队列,丢弃冲突的新增任务
        ThreadPoolExecutor executor = ThreadPoolUtil.fixedThreadExecutor("TimerWindow-flash", 1, "闪烁窗口线程");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        return executor;
    }

    private void initView() {
        timeView = new TimeView(this);
        setContentPane(timeView);
        windowMenu = new WindowMenu();
    }

    private void initListener() {
        //设置界面修改后触发
        config.addConfigChangedListener(this::applyConfig);

        TimerWindowGesture.bind(new GestureCallback() {

            @Override
            public Window getWindow() {
                return TimerWindow.this;
            }

            @Override
            public boolean disableWindowMenu() {
                return config.timeWindowConfig.disableWindowMenu;
            }

            @Override
            public boolean actBy2Click() {
                return config.actBy2Click;
            }

            @Override
            public void afterMoved() {
                log.info("");
                config.timeWindowConfig.loc = getLocation();
                config.save();
                afterLocationChanged();
            }

            /**
             * @apiNote 无可避免的与上次afterChangedAlpha的值产生误差!详见 {@link TimeViewConfig}
             */
            @Override
            public float getAlpha() {
                return config.timeViewConfig.getAlpha();
            }

            @Override
            public void afterChangedAlpha(float alpha) {
                log.info("alpha={}", alpha);
                config.timeViewConfig.afterChangedAlpha(alpha);
                config.save();
            }

            @Override
            public void onSizeLimit() {
                flashFrame(2);
            }

            @Override
            public void afterChangedSize(int fontSize) {
                log.debug("fontSize={}", fontSize);
                config.timeViewConfig.fontSize = fontSize;
                config.timeWindowConfig.loc = getLocation();
                config.save();
                afterLocationChanged();
            }

            @Override
            public void moveToBootLoc() {
                setLocation(bootLoc);
                afterMoved();
            }

            @Override
            public boolean isShowingMenu() {
                return windowMenu.isShowing();
            }

            @Override
            public void showMenu(MouseEvent e) {
                windowMenu.showMenu(e);
            }

            @Override
            public boolean isDisableWindowMenu() {
                return config.timeWindowConfig.disableWindowMenu;
            }

            @Override
            public void switchDisableWindowMenu() {
                config.timeWindowConfig.disableWindowMenu = !config.timeWindowConfig.disableWindowMenu;
                config.save();
            }
        });
    }

    private void applyConfig(TimerConfig config) {
        timeView.applyConfig(config.timeViewConfig);
        setSize(timeView.getViewSize());
        setLocation(config.timeWindowConfig.loc);
        FrameUtil.setAlpha(TimerWindow.this, config.timeViewConfig.getAlpha());
        log.debug("loc={}, size={}", getLocation(), getSize());
    }

    @Override
    public void setLocation(Point p) {
        /*
        处理超出屏幕范围的情况
        调用者:
            启动时,根据配置文件,设定位置
            恢复启动时位置
            恢复默认设置
        这些情况,都有可能由于分辨率在程序没启动时降低了,导致p在屏幕外
         */
        Dimension size = getSize();
        log.debug("p={}, size={}", p, size);
        Dimension screenSize = ScreenUtil.getScreenSize();
        int x = p.x;
        int y = p.y;
        if (x < 0) {
            x = 0;
        } else if (x + size.width > screenSize.width) {
            x = screenSize.width - size.width;
        }
        if (y < 0) {
            y = 0;
        } else if (y + size.height > screenSize.height) {
            y = screenSize.height - size.height;
        }
        p.x = x;
        p.y = y;
        super.setLocation(p);
        afterLocationChanged();
    }

    /**
     * 让窗口闪烁
     *
     * @param flashTime 闪烁次数
     */
    private void flashFrame(int flashTime) {
        flashExecutor.execute(() -> {
            log.info("flashTime={}", flashTime);
            for (int j = 1; j <= flashTime; j++) {
                for (float a = 0.2f; a <= 1; a += 0.1f) {
                    FrameUtil.setAlpha(this, a);
                    ThreadUtil.sleep(30);
                }
            }
            FrameUtil.setAlpha(this, config.timeViewConfig.getAlpha());
            doUpdateMousePenetration();
        });
    }

    /**
     * 记录本程序或用户修改位置后的信息;用于[跟随分辨率变化];
     * 解决问题:
     * (猜测的)系统级别的分辨率更新后[系统界面修改分辨率,切换屏幕,系统远程控制(猜测)],系统会更新所有窗口的位置,
     * 所以本程序监听到onScreenSizeChanged后,得到的窗口位置已经被系统改到屏幕内了,导致onScreenSizeChanged处理结果不对
     *
     * @apiNote 这里的调用逻辑比较混乱
     */
    private void afterLocationChanged() {
        /*
        本程序或用户主动修改的调用者:
            程序启动后,
            鼠标移动或缩放窗口后,
            处理完分辨率变化后,
            本类的setLocation后(包括几种本程序的调用者),这种情况会重复调用了本方法,但影响不大
         */
        lastLocation = getLocation();
        log.info("lastLocation={}", lastLocation);
    }

    public void reshow(boolean needRefresh) {
        setAlwaysOnTop(false);
        setVisible(false);

        setAlwaysOnTop(true);
        setVisible(true);
        toFront();
        if (needRefresh) {
            flashFrame(3);
        } else {
            doUpdateMousePenetration();
        }
    }

    /**
     * [设置]dialog实现[即时预览]效果;
     * 不更新字体和位置
     */
    public void applyPreviewConfig(TimeViewConfig previewConfig) {
        timeView.applyConfig(previewConfig);
        setSize(timeView.getViewSize());//处理[恢复默认]时,需要重置大小
        FrameUtil.setAlpha(TimerWindow.this, previewConfig.getAlpha());
    }

    @Override
    public void onScreenChanged(Dimension newer, Dimension older, DisplayMode newerMode, DisplayMode olderMode) {
        log.info("newer={}, older={}, scale={}", newer, older, ScreenUtil.getScreenScale());
        Point curLoc = getLocation();
        Point location = lastLocation;
        Dimension size = getSize();
        log.info("curLoc={}, lastLocation={}, size={}", curLoc, location, size);
        //新坐标:如果是边缘,则贴紧边缘;否则 按窗口中心比例计算新的坐标
        int x, y;
        if (location.x <= TimerConstant.EDGE_PX) {
            log.debug("edgeX");
            x = 0;
        } else if (location.x + size.width >= older.width - TimerConstant.EDGE_PX) {
            log.debug("edgeX");
            x = newer.width - size.width;
        } else {
            double ratio = (location.x + size.width / 2.0) / older.width;
            x = (int) (ratio * newer.width - size.width / 2.0);
            log.debug("ratioX={}", ratio);
            if (x < 0) {
                x = 0;
            } else if (x + size.width > newer.width) {
                x = newer.width - size.width;
            }
        }
        if (location.y <= TimerConstant.EDGE_PX) {
            log.debug("edgeY");
            y = 0;
        } else if (location.y + size.height >= older.height - TimerConstant.EDGE_PX) {
            log.debug("edgeY");
            y = newer.height - size.height;
        } else {
            double ratio = (location.y + size.height / 2.0) / older.height;
            y = (int) (ratio * newer.height - size.height / 2.0);
            log.debug("ratioY={}", ratio);
            if (y < 0) {
                y = 0;
            } else if (y + size.height > newer.height) {
                y = newer.height - size.height;
            }
        }
        log.info("x={}, y={}", x, y);
        setLocation(x, y);

        //这里也要保存位置:一般全屏是不用保存的,但系统级别的分辨率变化是要保存的
        config.timeWindowConfig.loc = getLocation();
        config.save();

        afterLocationChanged();
    }

    public void doUpdateMousePenetration() {
        if (!this.isVisible()) return;
        if (config.mousePenetration) {
            try {
                int[] res = FrameUtil.setMousePenetration(this);
                if (windowStyleDefault == null) {
                    //只能首次开启时赋值, 避免windowStyleDefault在某些情况下(多实例启动时)错乱了
                    windowStyleDefault = res[0];
                }
            } catch (Exception e) {
                onExecuteException("开启[鼠标穿透]", e);
            }
        } else {
            if (windowStyleDefault == null) return;
            try {
                FrameUtil.setWindowStyles(this, windowStyleDefault);
            } catch (Exception e) {
                onExecuteException("关闭[鼠标穿透]", e);
            }
        }
    }

    private void onExecuteException(String desc, Throwable e) {
        ErrInfo errInfo = AppExceptionHandler.handle(desc + "错误:\n", e);
        DialogHelper.confirm(errInfo.getErrMsg())
                .title(DialogHelper.ERROR)
                .show(this);
    }

    private class WindowMenu extends AutoHideMenu implements ActionListener {
        private static final String CMD_AUTO_ON_TOP = "定时置顶";
        private static final String CMD_HIDE = "隐藏";
        private static final String CMD_EXIT = "退出";
        private final JCheckBoxMenuItem cbAutoOnTop;

        public WindowMenu() {
            super(SpringContext.getBean(AppIcon.class).getImage());
            cbAutoOnTop = new JCheckBoxMenuItem(CMD_AUTO_ON_TOP);
            add(cbAutoOnTop).addActionListener(this);
            add(CMD_HIDE).addActionListener(this);
            add(CMD_EXIT).addActionListener(this);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            switch (e.getActionCommand()) {
                case CMD_AUTO_ON_TOP:
                    JCheckBoxMenuItem cb = (JCheckBoxMenuItem) e.getSource();
                    config.autoOnTop = cb.isSelected();
                    config.save();
                    config.fireConfigChanged();
                    return;
                case CMD_HIDE:
                    presenter.beforeSwitchWindowVisible(false);
                    TimerWindow.this.dispose();
                    return;
                case CMD_EXIT:
                    SpringContext.getBean(SimpleExitHandler.class).doExit();
            }
        }

        public void showMenu(MouseEvent e) {
            cbAutoOnTop.setSelected(config.autoOnTop);
            show(TimerWindow.this, e.getX(), e.getY());
        }
    }

}
